<?php
/**
 * Project: AIGC
 * Author: moufer <moufer@163.com>
 * Date: 2024/7/20 23:03
 */

namespace module\chat\logic;

use module\ai\library\channel\Completion;
use module\ai\library\channel\exception\ChatException;
use module\ai\library\channel\Factory;
use module\ai\model\Channel;
use module\chat\front\searcher\MessageSearcher;
use module\chat\model\Message;
use module\user\logic\UserLogic;
use module\user\model\User;
use mof\annotation\Inject;
use mof\exception\LogicException;
use mof\interface\UserInterface;
use mof\Logic;
use mof\Model;
use OpenAI\Responses\Chat\CreateResponse;
use OpenAI\Responses\Chat\CreateStreamedResponse;
use OpenAI\Responses\StreamResponse;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\facade\Cache;
use think\facade\Log;

class MessageLogic extends Logic
{
    /**
     * @var Message
     */
    #[Inject(Message::class)]
    protected $model;

    /**
     * @var User 当前对话用户
     */
    protected UserInterface $user;

    public function withUser(UserInterface $user): static
    {
        $this->user = $user;
        return $this;
    }

    public function histories(MessageSearcher $searcher): array
    {
        $paginator = $searcher->paginate(true);
        $collection = $paginator->getCollection()->load(['channel']);
        $collection->append(['prompt', 'receive'])->visible(['id', 'prompt', 'receive', 'channel']);

        $result = $paginator->toArray();
        $result['data'] = array_map(function ($item) {
            $item['prompt']['channel'] = $item['channel'];
            return array_intersect_key($item, array_flip(['id', 'prompt', 'receive']));
        }, $result['data']);

        return $result;
    }

    /**
     * 清除上下文，即下一轮重新对话
     * @param int $id
     * @return void
     * @throws DataNotFoundException
     */
    public function clearContext(int $id): void
    {
        $model = $this->withAccess(['user_id' => $this->user->getId()])->read($id);
        $model->save(['next_anew' => 1]);
    }

    /**
     * 删除指定用户里的所有记录
     * @param $params
     * @return int
     */
    public function deleteAll($params): int
    {
        if (!$params) {
            throw new LogicException('参数不完整');
        }
        $params['user_id'] = $this->user->getId();
        return $this->model::class::where($params)->delete();
    }

    public function save($params): Model
    {
        if (!$channel = Channel::find($params['channel_id'])) {
            throw new LogicException('AI通道无效');
        }
        if ($params['object_type'] === 'assistant') {
            //创作助手不提供携带历史记录，只有一轮对话
            $params['is_first'] = 1;
        }
        if (empty($params['is_first'])) {
            //获取上一条消息
            $where = [
                'user_id'     => $this->user->getId(),
                'object_type' => $params['object_type'],
                'object_id'   => $params['object_id'],
                'channel_id'  => $params['channel_id'],
                'received'    => 1
            ];
            //上一条记录
            $prev = $this->model->where($where)->order('id', 'desc')->find();
            if ($prev && $prev->session) {
                $params['session'] = $prev->session;
                $params['index'] = $prev->index + 1;
            }
        }
        //生成session，用于携带历史记录
        if (empty($params['session'])) {
            $params['session'] = md5(uniqid());
            $params['index'] = 1;
        }
        //消费能力判断
        $this->checkConsumption($channel);

        return parent::save($params);
    }

    /**
     * @param int $id
     * @return Completion
     * @throws DataNotFoundException
     * @throws DbException
     * @throws ModelNotFoundException
     */
    public function completion(int $id): Completion
    {
        /** @var Message $message */
        $message = parent::withAccess(['user_id' => $this->user->getId()])->read($id);
        $response = $this->getChatResponse($message);
        $completion = Completion::make($response);
        $this->saveReceive($message, $completion);
        return $completion;
    }

    /**
     * @param int $id
     * @param callable $callback
     * @return Completion
     * @throws DataNotFoundException
     * @throws DbException
     * @throws ModelNotFoundException
     */
    public function completionStream(int $id, callable $callback): Completion
    {
        /** @var Message $message */
        $cancelKey = 'chat_completion_' . $id;
        $message = parent::withAccess(['user_id' => $this->user->getId()])->read($id);
        $result = null;
        $streamResponse = $this->getChatResponse($message, true);
        Cache::set($cancelKey, 0, 300);
        /** @var CreateStreamedResponse $response */
        foreach ($streamResponse as $response) {
            $completion = Completion::make($response);
            $result = $result ? $result->merge($completion) : $completion;
            $chunks = $completion->sseChunks();
            $isCancel = false;
            foreach ($chunks as $chunk) {
                //检测是否取消
                $isCancel = Cache::get($cancelKey, 0);
                $callback($isCancel ? $completion->withFinishReason('cancel')->sse() : $chunk);
                if ($isCancel) break;
            }
            if ($isCancel) {
                $result->withFinishReason('cancel');
                break;
            }
        }
        if (!$result) {
            throw new ChatException('未知错误');
        }
        $this->saveReceive($message, $result);
        //删除缓存
        Cache::delete($cancelKey);
        return $result;
    }

    public function cancelReceive($id): void
    {
        $cancelKey = 'chat_completion_' . $id;
        if (Cache::has($cancelKey)) {
            Cache::set($cancelKey, 1, 60);
        }
    }

    /**
     * 获取大语言模型
     * @param Message $message
     * @param bool $isStream
     * @return CreateResponse|StreamResponse
     * @throws DataNotFoundException
     * @throws DbException
     * @throws ModelNotFoundException
     */
    protected function getChatResponse(Message $message, bool $isStream = false): CreateResponse|StreamResponse
    {
        if ($message->received) {
            //throw new ChatException('不可重复接收消息');
        }
        if (!$message->channel || $message->channel->status !== 1) {
            throw new ChatException('消息通道不存在或已下架');
        }

        if (!$message->channel->oneKey()) {
            throw new ChatException('接口密钥未配置');
        }

        $message->save([
            'key_id' => $message->channel->oneKey()->id
        ]);

        $config = $message->channel->config;
        $chat = Factory::chat($message->channel);
        //设置角色信息
        if (!empty($config['system'])) {
            $chat->withSystem($config['system']);
        } else {
            $chat->withSystem('你是由磨锋软件开发工作室提供的AI助手，你更擅长中文和英文的对话。' .
                '你会为用户提供安全，有帮助，准确的回答。' .
                '同时，你会拒绝一切涉及恐怖主义，种族歧视，黄色暴力等问题的回答。');
        }
        //增加历史对话
        $historyFreq = (int)config('chat.history_chat_freq', 1);
        if ($historyFreq > 0 && $message->index > 1) {
            $where = [
                'user_id'     => $this->user->id,
                'session'     => $message->session,
                'received'    => 1, //已接收的记录
                'user_cancel' => 0, //去掉用户取消的记录
            ];
            $histories = $this->model->where($where)
                ->order(['id' => 'desc'])
                ->limit($historyFreq)
                ->select()
                ->reverse(); //反转，最新的数据放在后面加入

            foreach ($histories as $history) {
                $chat->withHistory($this->assemblyPrompt($history), $history->receive_text);
            }
            $message->carry_num = count($histories);//携带历史消息轮数(1轮标识2条消息)
        }

        $prompt = $this->assemblyPrompt($message);
        if ($isStream) {
            return $chat->createStreamed($prompt, $message->channel->config, true);
        }
        return $chat->create($prompt, $message->channel->config);
    }

    /**
     * 提问模版填充
     * @param Message $message
     * @return string
     */
    protected function assemblyPrompt(Message $message): string
    {
        if ($message->index === 1 && in_array($message->object_type, ['assistant', 'visrole'])) {
            $search = ['{prompt}', '{lang}'];
            $replace = [$message->prompt_text, $message->lang ?: '中文'];
            $template = $message->object?->config['template'] ?? $message->prompt_text;
            return str_replace($search, $replace, $template);
        }
        return $message->prompt_text;
    }

    /**
     * 保存回复内容
     * @param Message $message
     * @param Completion $completion
     * @return void
     */
    protected function saveReceive(Message $message, Completion $completion): void
    {
        try {
            $message->user_cancel = $completion->finishReason() === 'cancel';
            $message->received = 1;
            $message->receive_text = $completion->message();
            $message->receive_errcode = 0;
            $message->receive_at = date('Y-m-d H:i:s');
            $message->send_tokens = $completion->tokens('prompt_tokens');
            $message->receive_tokens = $completion->tokens('completion_tokens');
            $message->total_tokens = $completion->tokens('total_tokens');
            $message->save();
            //扣除积分
            $this->expense($message);
            //触发数据累加时间
            event('system.total', ['name' => "chat.{$message->object_type}", 'step' => 1]);
            //触发对话完成事件
            event('chat.complete', $message);
        } catch (DbException $e) {
            Log::error($e->getMessage());
        }

    }

    /**
     * 检测是否有消费余额
     * @param Channel $channel
     * @return void
     */
    protected function checkConsumption(Channel $channel): void
    {
        $score = app(ConfigLogic::class)->getConsumptionScore($channel, $this->user);
        if ($this->user->score < $score) {
            throw new LogicException('抱歉，您的积分不足');
        }
    }

    /**
     * 消费积分
     * @param Message $message
     * @return void
     */
    protected function expense(Message $message): void
    {
        $score = 0;
        try {
            $score = app(ConfigLogic::class)->getConsumptionScore($message->channel, $this->user);
            if ($this->user->score < $score) {
                //throw new LogicException('抱歉，您的积分不足', 1001);
                //积分不足的情况下，只能扣除他所有剩余积分（如果还有积分的话）
                $score = $this->user->score;
            }
        } catch (LogicException $e) {
            //报错继续，避免影响正常流程。下次对话，会再次检测
        }
        //扣除积分
        if ($score > 0) {
            app(UserLogic::class)->withUser($this->user)
                ->decScore($score, 'chat', '对话', 'chat');
        }
    }

}